weixin_39865625
2021-01-12 13:05 阅读 1

Intermittent 401 Unauthorized errors

Sometimes works, sometimes doesn't. Restarting the app helps. Could this be because there is a long lag time between an authorization of a credential with gspread.authorize and then taking actions on the session?


HTTPError: 401: 

<title>Unauthorized</title>


<h1>Unauthorized</h1>
<h2>Error 401</h2>


Traceback (most recent call last):
  File "/home/mark/timeobserver/lib/python2.7/site-packages/flask/app.py", line 1836, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File "/home/mark/timeobserver/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/mark/timeobserver/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/flask/app.py", line 1381, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/mark/timeobserver/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/mark/timeobserver/server.py", line 42, in toggleWork
    worksheet.append_row([session.timeStarted, datetime.datetime.now(), session.durationWorked, session.durationInterrupted, datetime.datetime.now() - session.timeStarted, session.interruptions])
  File "/home/mark/timeobserver/lib/python2.7/site-packages/gspread/models.py", line 525, in append_row
    self.add_rows(1)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/gspread/models.py", line 507, in add_rows
    self.resize(rows=self.row_count + rows)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/gspread/models.py", line 488, in resize
    feed = self.client.get_feed(self_uri)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/gspread/client.py", line 256, in get_feed
    r = self.session.get(url)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/gspread/httpsession.py", line 75, in get
    return self.request('GET', url, **kwargs)
  File "/home/mark/timeobserver/lib/python2.7/site-packages/gspread/httpsession.py", line 71, in request
    response.status_code, response.content))

gspread latest (0.3.0)

该提问来源于开源项目:burnash/gspread

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享

31条回答 默认 最新

  • weixin_39851279 weixin_39851279 2021-01-12 13:05

    I am getting the same issue now and was wondering if there is a solution?

    点赞 评论 复制链接分享
  • weixin_39789979 weixin_39789979 2021-01-12 13:05

    Getting the same error, but TBH, my python script runs for hours, so I suspect that we have to reauthenticate every once in a while? Is there a way to make a persistent authorization connection, so we don't have to reauthenticate every N hours?

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    I am also having the same issue, I am using this in my web app, which is working on google app engine. I can solve the issue for a while by re-deploying my web app, but it starts happening again after a while. I dont know if its about reauthenticating, since I tried that and there was no change. So please if you find solution, let me know.

    点赞 评论 复制链接分享
  • weixin_39865625 weixin_39865625 2021-01-12 13:05

    I'm not sure if this will help in other cases, but the way that I solved this was to have authentication (with gspread.authorize) happen right before I executed an action. So, instead of authenticating in the beginning of my script, and then using those details to execute an action, I authenticate before every action. I'm curious if there is a persistent way to do this as says.

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    , I am still facing the problem, even after using gspread.authorize at the beginning of every action. :'( why is it doing this to me? if you know any other solution, please help me. (but thanks for the reply, I have been posting this problem in few forums: stackoverflow, google groups, but wasnt getting any response)

    点赞 评论 复制链接分享
  • weixin_40000131 weixin_40000131 2021-01-12 13:05

    What's your gspread version?

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    0.2.5

    点赞 评论 复制链接分享
  • weixin_40000131 weixin_40000131 2021-01-12 13:05

    Could you update to the latest version and try again?

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    Looks like I get a new error if I update to the latest version :

    
    FeatureNotEnabledError: The Socket API will be enabled for this application once billing has been enabled in the admin console.
    at create_connection (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/packages/urllib3/util/connection.py:67)
    at _new_conn (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/packages/urllib3/connection.py:142)
    at connect (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/packages/urllib3/connection.py:254)
    at _validate_conn (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/packages/urllib3/connectionpool.py:814)
    at _make_request (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/packages/urllib3/connectionpool.py:351)
    at urlopen (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/packages/urllib3/connectionpool.py:578)
    at send (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/adapters.py:403)
    at send (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/sessions.py:585)
    at request (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/sessions.py:475)
    at get (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/requests/sessions.py:487)
    at request (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/gspread/httpsession.py:68)
    at get (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/gspread/httpsession.py:76)
    at get_spreadsheets_feed (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/gspread/client.py:165)
    at open (/base/data/home/apps/s~gowcanteen/1.394383211349002244/lib/gspread/client.py:81)
    at __init__ (/base/data/home/apps/s~gowcanteen/1.394383211349002244/server.py:13)
    at <module> (/base/data/home/apps/s~gowcanteen/1.394383211349002244/main.py:15)
    </module>

    Now I am only student at the moment and its possible for me to enable the billing on my own. Well this was something for my school, so will talk to my teacher and see if he is go for it or not. If there is any other way to solve this, please let me know. And many thanks for replying

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    And also I am not sure if the latest version will solve that problem or not? I am very curious what is the problem and why is it occuring? Especially why does it run properly for a while after redeploying, and then after some hours (which isnt a constant amount) it starts giving HTTPError 401?

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    Moved from #402

    I have faced quite the same issue today: after a night running gspread refused to modify the spreasheet. Unfortunately I have no logs, because it was running not in main thread but I guess the problem is that Google session expires. Have anyone tried to recreate a credentials instance? I'm going to try it out.

    My idea is to catch an HTTPError performing the action (inserting row in my case) and recreate the credentials to attempt once again.

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    if it works out share the code with me plz for now all i do is re upload the code every day

    On Wed, 10 Aug 2016 6:48 pm Anton Bryzgalov (aka Mashkoff), < notifications.com> wrote:

    I have faced quite the same issue today: after a night running gspread refused to modify the spreasheet. Unfortunately I have no logs, because it was running not in main thread but I guess the problem is that Google session expires. Have anyone tried to recreate a credentials instance? I'm going to try it out.

    My idea is to catch an HTTPError and recreate the credentials to attempt once again.

    — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/burnash/gspread/issues/373#issuecomment-238855875, or mute the thread https://github.com/notifications/unsubscribe-auth/AGTx3YN8zPnoNWLqkw-nH1GtzVkZzZJEks5qech-gaJpZM4H2StP .

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    As mentioned in Google Cloud Platform docs, one of the common reasons for 401 error is:

    The OAuth access token has expired and needs to be refreshed. This can be avoided by refreshing the access token early, but code can also catch this error, refresh the token and retry automatically.

    That's why I decided to create such a class instead of gspread Worksheet class:

     python
    class WKS:
        def __init__(self):
            # logger settings
            self.logger = logging.getLogger(self.__class__.__name__)
            self.logger.setLevel(logging.INFO)
            file_handler = logging.FileHandler(self.__class__.__name__ + '.log', encoding='utf-8')
            file_handler.setLevel(logging.INFO)
            file_handler.setFormatter(log_formatter)
            self.logger.addHandler(file_handler)
    
            self._refresh_auth()
    
        def _refresh_auth(self):
            scope = ['https://spreadsheets.google.com/feeds']
            credentials = ServiceAccountCredentials.from_json_keyfile_name(secret.GOOGLE_JSON_FILENAME, scope)
            gc = gspread.authorize(credentials)
            self.wks = gc.open_by_key(secret.SPREADSHEET_KEY).sheet1
            self.logger.info('New auth has been performed.')
    
        def _decorate(self, method):
            def safe_method(*args, **kwargs):
                try:
                    method(*args, **kwargs)
                except gspread.exceptions.HTTPError as e:
                    self.logger.info('HTTPError: code = ' + str(e.code) + '\n' + e.args[0])
                    self._refresh_auth()
                    try:
                        method(*args, **kwargs)
                    except:
                        self.logger.error('Error while handling: ' + str(sys.exc_info()))
                except:
                    self.logger.error('Unexpected error: ' + str(sys.exc_info()))
            return safe_method
    
        def __getattr__(self, attr):  # only called if there was no possible explicit call
            self.logger.info(attr + ' method')
            return self._decorate(getattr(self.wks, attr))
    

    Frankly speaking, I haven't tried it in production but I hope it will help.

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    thanks will try adding to my code

    On Thu, 11 Aug 2016 12:35 am Anton Bryzgalov (aka Mashkoff), < notifications.com> wrote:

    As mentioned https://cloud.google.com/storage/docs/json_api/v1/status-codes#401_Unauthorized in Google Cloud Platform docs, one of the common reasons for 401 error is:

    The OAuth access token has expired and needs to be refreshed. This can be avoided by refreshing the access token early, but code can also catch this error, refresh the token and retry automatically.

    That's why I decided to create such a class instead of gspread Worksheet class:

    class WKS: def init(self): scope = ['https://spreadsheets.google.com/feeds'] self.credentials = ServiceAccountCredentials.from_json_keyfile_name(secret.GOOGLE_JSON_FILENAME, scope) self._refresh_auth()

    
    def _refresh_auth(self):
        gc = gspread.authorize(self.credentials)
        self.wks = gc.open_by_key(secret.SPREADSHEET_KEY).sheet1
    <p>def <strong>getattr</strong>(self, attr):
        if hasattr(self.wks, attr):
            try:
                return getattr(self.wks, attr)
            except gspread.exceptions.HTTPError as e:
                logging.warning('HTTPError: code = ' + str(e.code) + '\n' + e.args[0])
                self._refresh_auth()
                return getattr(self.wks, attr)
            except Exception as e:
                logging.error(str(e.args))
        else:
            return getattr(self, attr)
    </p>

    Frankly speaking, I haven't tried it in production but I hope it will help.

    — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/burnash/gspread/issues/373#issuecomment-238961120, or mute the thread https://github.com/notifications/unsubscribe-auth/AGTx3W-5ZTwaQalGzeMgQY93sE_85mm_ks5qehnvgaJpZM4H2StP .

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    , note please that I have just edited the snippet in the comment a bit to prevent stack overflow.

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    ok thanks for mentioning. i havent added it yet. plannig to do it tonight

    On Thu, 11 Aug 2016 1:26 am Anton Bryzgalov (aka Mashkoff), < notifications.com> wrote:

    https://github.com/rhemon, note please that I have edited the snippet in the comment a bit to prevent stack overflow.

    — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/burnash/gspread/issues/373#issuecomment-238976577, or mute the thread https://github.com/notifications/unsubscribe-auth/AGTx3YoC1Q3mwEghJvjJPRGL4Wq4_1_zks5qeiXpgaJpZM4H2StP .

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    Note that I have just changed code again. There was a senseless handling :laughing:

    However, recreating the credentials doesn't help. I looked through the code and found the only instance, from where could get such an output. It's here. I'm still trying to find out the cause of an error.

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    not sure exactly why but I have noticed that this error automatically goes away after a while and then comes back and again goes away and the cycle continues and I just realized that I am authorizing the credentials every time before every action when requests come, so wouldnt the result be same if I refresh the auth right after getting the error? so yeah i guess thats not working. btw what version are you using? I am using an old version, since latest version use a feature that requires billing to be enabled. So I am curious whether the latest version fixes the issue or not, if its a fix for sure I could talk to my teacher at school to enable billing for the project and use the latest version of gspread

    On Thu, Aug 11, 2016 at 10:19 PM Anton Bryzgalov (aka Mashkoff) < notifications.com> wrote:

    Not that I have just changed code again. There was senseless handling 😆

    However, recreating the credentials doesn't help. I looked through the code and found the only instance, from where https://github.com/markbao could get such an output. It's here https://github.com/burnash/gspread/blob/7627c98269e177f12715c71f38395db9268264a0/gspread/httpsession.py#L71. I'm still trying to find out the cause of an error.

    — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/burnash/gspread/issues/373#issuecomment-239212084, or mute the thread https://github.com/notifications/unsubscribe-auth/AGTx3SBsAXN6ZQka0aBEWK16vhyYx2uaks5qe0umgaJpZM4H2StP .

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    I'm currently using the latest version of gspread from pip: 0.4.1. The problem is certainly caused by credentials expiration after an hour script running. Right now I have found out an interesting thing, using my code from above: the insert_row method, which I'm using, worked fine after an hour of script running in the following chain of calls: - insert_row (caught an HTTPError in outer try-block) - _refresh_auth() (called from handler) - insert_row (HTTPError again, caught in the inner try-block) - insert_row (new attempt, succeed this time)

    I have no idea how to explain it.

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    I have understood the reason: I have been still using the old (non-authorized properly) instance of worksheet, that's why only the new attempt succeed. So, I guess, proper reauthorizing is the solution for the issue.

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    I have tested the snippet and here is the final solution, that works for me (proxy class for worksheet):

     python
    class WKS:
        def __init__(self):
            scope = ['https://spreadsheets.google.com/feeds']
            self.credentials = ServiceAccountCredentials.from_json_keyfile_name(secret.GOOGLE_JSON_FILENAME, scope)
            self._refresh_auth()
    
        def _refresh_auth(self):
            gc = gspread.authorize(self.credentials)
            self.wks = gc.open_by_key(secret.SPREADSHEET_KEY).sheet1
    
        def _decorate(self, method):
            def safe_method(*args, **kwargs):
                try:
                    method(*args, **kwargs)
                except gspread.exceptions.HTTPError as e:
                    # getattr is needed to get a new instance of self.wks
                    self._refresh_auth()
                    # UPD: fix by 
                    return getattr(self.wks, method.__name__)(*args, **kwargs) 
            return safe_method
    
        def __getattr__(self, attr):  # doesn't shadow _refresh_auth and _decorate
            return self._decorate(getattr(self.wks, attr))
    
    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    Ok thanks.. added to my code, hope it will solve it , my code is some what like this: method: try: actions of spreadsheet count = 0 except HTTPError: refresh_auth count += 1 if (count =< 10): method i added the count thing just incase (maybe its not really needed) so that it doesnt keep iterating if the error doesnt go away

    On Fri, Aug 12, 2016 at 2:34 AM Anton Bryzgalov (aka Mashkoff) < notifications.com> wrote:

    I have tested the snippet and here is the final solution, that works for me (proxy class for worksheet):

    class WKS: def init(self): scope = ['https://spreadsheets.google.com/feeds'] self.credentials = ServiceAccountCredentials.from_json_keyfile_name(secret.GOOGLE_JSON_FILENAME, scope) self._refresh_auth()

    
    def _refresh_auth(self):
        gc = gspread.authorize(self.credentials)
        self.wks = gc.open_by_key(secret.SPREADSHEET_KEY
    

    ).sheet1

    
    def _decorate(self, method):
        def safe_method(<em>args, </em><em>kwargs):
            try:
                method(</em>args, <strong>kwargs)
            except gspread.exceptions.HTTPError as e:
                # getattr is needed to get a new instance of self.wks
                getattr(self.wks, method.<strong>name</strong>)(*args, </strong>kwargs)
        return safe_method
    <p>def <strong>getattr</strong>(self, attr):  # doesn't shadow _refresh_auth and _decorate
        return self._decorate(getattr(self.wks, attr))
    </p>

    — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/burnash/gspread/issues/373#issuecomment-239283469, or mute the thread https://github.com/notifications/unsubscribe-auth/AGTx3VybGV4KJDu1yWeIUmkp2UseuwLdks5qe4dCgaJpZM4H2StP .

    点赞 评论 复制链接分享
  • weixin_39638603 weixin_39638603 2021-01-12 13:05

    Where does your line "refresh_auth" come from? I'm new to this and trying to use your technique to fix this issue. Did you add the script by bryzgaloff as well or just the error exception? I have a good idea of where I'd need to put your script, but I don't know if there's a certain spot where bryzgaloff's script would go if necessary.

    点赞 评论 复制链接分享
  • weixin_39638603 weixin_39638603 2021-01-12 13:05

    I saw your possible solution to this HTTPError 401. I've never done anything like you wrote in your solution, so I have a couple questions.

    It looks like this goes in the beginning of my program in place of where I was authenticating before. Correct?

    You say 'proxy class by worksheet'. Are you creating WKS to act in place of worksheet? If I use your script, does this mean that I'd make these changes:

    worksheet.update_cell(1, 7, 'text') would become WKS.update_cell(1, 7, 'text') worksheet.append_row() would become WKS.append_row()

    I tried replacing 'worksheet' with 'WKS', but I ended up getting this error:

    
    Traceback (most recent call last):
      File "/home/bwatkin79/fangraphstest/fangraphsxpath.py", line 41, in <module>
        WKS.update_cell(1, 7, 'Home')
    AttributeError: type object 'WKS' has no attribute 'update_cell'
    </module>

    Please help. I'm trying to see if your solution will work for me too.

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    refreh_auth is a function that i defined which just authorizes and opens the spreadsheet fine again, same thing bryzgaloff has done in his _refresh_auth method

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 2021-01-12 13:05

    and it seems the solution works, but still sometimes i get the httperror but right after that if i retry the request, it works, so its kinda working for me

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    excuse me for late answer. Here are the instructions: - You have to define my WKS class as a usual Python class to use it then in your code. Simply insert it into your code before usage. - Next, create an instance of the WKS class like that:

     python
    worksheet = WKS()
    

    Please notice that the secret key for opening the spreadsheet in my code is hardcoded in secret package as secret.GOOGLE_JSON_FILENAME. So you have to create the same structure or, for instance, re-implement my __init__ method and make the key to be a parameter of the function. - After all you can now use created worksheet object (instance of WKS class) as a replacement for your previous worksheet (instance of gspread.Worksheet class).

    Your mistake was to use WKS class instead of its instance. That's why you have got an error, because the class itself doesn't have attribute update_cell but an instance of it does.

    点赞 评论 复制链接分享
  • weixin_39775428 weixin_39775428 2021-01-12 13:05

    Hi, thanks for the help

    I tried modifying your code a bit but now I'm getting None returned

    python
    import gspread
    from oauth2client.service_account import ServiceAccountCredentials
    
    
    class WKS:
        def __init__(self):
            scope = ['https://spreadsheets.google.com/feeds']
            self.credentials = ServiceAccountCredentials.from_json_keyfile_name('json.json', scope)
            self.spreadsheet_key = 'key'
            self.worksheet_name = 'Sheet24'
            self._refresh_auth()
    
        def _refresh_auth(self):
            gc = gspread.authorize(self.credentials)
            self.wks = gc.open_by_key(self.spreadsheet_key).worksheet(self.worksheet_name)
    
        def _decorate(self, method):
            def safe_method(*args, **kwargs):
                try:
                    method(*args, **kwargs)
                except gspread.exceptions.HTTPError as e:
                    # getattr is needed to get a new instance of self.wks
                    getattr(self.wks, method.__name__)(*args, **kwargs)
            return safe_method
    
        def __getattr__(self, attr):  # doesn't shadow _refresh_auth and _decorate
            return self._decorate(getattr(self.wks, attr))
    
    
    worksheet = WKS()
    
    data = worksheet.range(1, 1, 5, 2)
    
    print(data)
    

    I'm really new to Python, I'm still trying to wrap my head around classes and instances.

    thanks!

    点赞 评论 复制链接分享
  • weixin_39998906 weixin_39998906 2021-01-12 13:05

    There's a little mistake in code; basically, there's returns missing in safe_method.

    This should work:

    python
    import gspread
    from oauth2client.service_account import ServiceAccountCredentials
    
    class WKS:
        def __init__(self):
            scope = 'https://www.googleapis.com/auth/spreadsheets'
            self.credentials = ServiceAccountCredentials.from_json_keyfile_name('./client_secret.json', scope)
            self._refresh_auth()
    
        def _refresh_auth(self):
            gc = gspread.authorize(self.credentials)
            self.wks = gc.open_by_key(config["gsheet_key"]).sheet1
    
        def _decorate(self, method):
            def safe_method(*args, **kwargs):
                try:
                    return method(*args, **kwargs)
                except gspread.exceptions.HTTPError as e:
                    # getattr is needed to get a new instance of self.wks
                    self._refresh_auth()
                    return getattr(self.wks, method.__name__)(*args, **kwargs)
            return safe_method
    
        def __getattr__(self, attr):  # doesn't shadow _refresh_auth and _decorate
            return self._decorate(getattr(self.wks, attr))
    

    Edit: tested it in production and made some changes

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 2021-01-12 13:05

    thanks for your fix, I've updated my comment too

    点赞 评论 复制链接分享
  • weixin_40000131 weixin_40000131 2021-01-12 13:05

    This should be fixed with #637. On PyPI since version 3.4.0.

    I'm closing this issue. Please upgrade gspread. If the problem persists, ping me and I'll reopen the issue.

    点赞 评论 复制链接分享

相关推荐