Repository: burnash/gspread

Issue:
Client has no attribute set_timeout
**Describe the bug**
Using gspread 6.0.0 I have a `'Client' object has no attribute 'set_timeout'` error.

**To Reproduce**
```
        import gspread
        gspread_service = gspread.service_account(GSHEET_SERVICE_ACCOUNT_PATH)
        gspread_service.set_timeout(120)
```

**Expected behavior**
Update the timeout


**Screenshots**
![image](https://github.com/burnash/gspread/assets/229453/bdab3254-6689-4c5a-ab86-898b747a5b72)


**Environment info:**
- Operating System **Linux**
- Python version **3.11**
- gspread version **6.0.0**



Discussion:
lavigne958: > looks good
> 
> was this method in Worksheet and Spreadsheet before? if not, why do we add it? it is not needed for backwards compatibility
not it was not, I thought I add extra ways to set timeout for users so it offer more entry point to set the timeout.
agreed it's not needed for backward compatibility.

> 
> The tests should be updated to reflect this change. We added a test for the missing client methods if I remember

I will check if I can add that, may be a new test that set a low timeout, so one day if we move it the test will fail when trying to call a method that does not exists, do you think this will be enough ? :thinking: 


lavigne958: Actually after taking a look I managed to add a test that _tests_ the `set_timeout` method and measures the time too in case we don't respect the timeout value. :partying_face: 
lavigne958: I was thinking, maybe it's better if I just add the missing method and nothing else. We'll see in the future to add other convenient methods ? It makes things simpler 

Patch:
diff --git a/gspread/client.py b/gspread/client.py
index d7c89eb..35d622d 100644
--- a/gspread/client.py
+++ b/gspread/client.py
@@ -43,6 +43,17 @@ class Client:
     ) -> None:
         self.http_client = http_client(auth, session)
 
+    def set_timeout(self, timeout: Optional[Union[float, Tuple[float, float]]] = None):
+        """How long to wait for the server to send
+        data before giving up, as a float, or a ``(connect timeout,
+        read timeout)`` tuple.
+
+        Use value ``None`` to restore default timeout
+
+        Value for ``timeout`` is in seconds (s).
+        """
+        self.http_client.set_timeout(timeout)
+
     def get_file_drive_metadata(self, id: str) -> Any:
         """Get the metadata from the Drive API for a specific file
         This method is mainly here to retrieve the create/update time
diff --git a/gspread/http_client.py b/gspread/http_client.py
index 7f5b2f6..1973887 100644
--- a/gspread/http_client.py
+++ b/gspread/http_client.py
@@ -6,6 +6,7 @@ This module contains HTTPClient class responsible for communicating with
 Google API.
 
 """
+
 import time
 from http import HTTPStatus
 from typing import (
@@ -88,11 +89,13 @@ class HTTPClient:
 
         self.session.headers.update({"Authorization": "Bearer %s" % self.auth.token})
 
-    def set_timeout(self, timeout: Union[float, Tuple[float, float]]) -> None:
+    def set_timeout(self, timeout: Optional[Union[float, Tuple[float, float]]]) -> None:
         """How long to wait for the server to send
         data before giving up, as a float, or a ``(connect timeout,
         read timeout)`` tuple.
 
+        Use value ``None`` to restore default timeout
+
         Value for ``timeout`` is in seconds (s).
         """
         self.timeout = timeout
diff --git a/gspread/spreadsheet.py b/gspread/spreadsheet.py
index 8c57033..bad613d 100644
--- a/gspread/spreadsheet.py
+++ b/gspread/spreadsheet.py
@@ -7,9 +7,10 @@ This module contains common spreadsheets' models.
 """
 
 import warnings
-from typing import Union
+from typing import Any, Dict, Union
 
 from .exceptions import WorksheetNotFound
+from .http_client import HTTPClient
 from .urls import DRIVE_FILES_API_V3_URL, SPREADSHEET_DRIVE_URL
 from .utils import ExportFormat, finditem
 from .worksheet import Worksheet
@@ -18,7 +19,7 @@ from .worksheet import Worksheet
 class Spreadsheet:
     """The class that represents a spreadsheet."""
 
-    def __init__(self, http_client, properties):
+    def __init__(self, http_client: HTTPClient, properties: Dict[str, Any]):
         self.client = http_client
         self._properties = properties
 


Test patch:
diff --git a/tests/cassettes/ClientTest.test_add_timeout.json b/tests/cassettes/ClientTest.test_add_timeout.json
new file mode 100644
index 0000000..89a634a
--- /dev/null
+++ b/tests/cassettes/ClientTest.test_add_timeout.json
@@ -0,0 +1,305 @@
+{
+    "version": 1,
+    "interactions": [
+        {
+            "request": {
+                "method": "POST",
+                "uri": "https://www.googleapis.com/drive/v3/files?supportsAllDrives=True",
+                "body": "{\"name\": \"Test ClientTest test_add_timeout\", \"mimeType\": \"application/vnd.google-apps.spreadsheet\"}",
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "Content-Length": [
+                        "99"
+                    ],
+                    "Content-Type": [
+                        "application/json"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "Date": [
+                        "Wed, 21 Feb 2024 21:21:08 GMT"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "Vary": [
+                        "Origin, X-Origin"
+                    ],
+                    "Pragma": [
+                        "no-cache"
+                    ],
+                    "Expires": [
+                        "Mon, 01 Jan 1990 00:00:00 GMT"
+                    ],
+                    "Cache-Control": [
+                        "no-cache, no-store, max-age=0, must-revalidate"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "content-length": [
+                        "186"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"kind\": \"drive#file\",\n  \"id\": \"1ayf3YSjjjwDhFkUr3nrdN1JRScl3cOMB9lDbHEkjoko\",\n  \"name\": \"Test ClientTest test_add_timeout\",\n  \"mimeType\": \"application/vnd.google-apps.spreadsheet\"\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "GET",
+                "uri": "https://sheets.googleapis.com/v4/spreadsheets/1ayf3YSjjjwDhFkUr3nrdN1JRScl3cOMB9lDbHEkjoko?includeGridData=false",
+                "body": null,
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "Date": [
+                        "Wed, 21 Feb 2024 21:21:08 GMT"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "x-l2-request-path": [
+                        "l2-managed-6"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "Vary": [
+                        "Origin",
+                        "X-Origin",
+                        "Referer"
+                    ],
+                    "Cache-Control": [
+                        "private"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "content-length": [
+                        "3330"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"spreadsheetId\": \"1ayf3YSjjjwDhFkUr3nrdN1JRScl3cOMB9lDbHEkjoko\",\n  \"properties\": {\n    \"title\": \"Test ClientTest test_add_timeout\",\n    \"locale\": \"en_US\",\n    \"autoRecalc\": \"ON_CHANGE\",\n    \"timeZone\": \"Etc/GMT\",\n    \"defaultFormat\": {\n      \"backgroundColor\": {\n        \"red\": 1,\n        \"green\": 1,\n        \"blue\": 1\n      },\n      \"padding\": {\n        \"top\": 2,\n        \"right\": 3,\n        \"bottom\": 2,\n        \"left\": 3\n      },\n      \"verticalAlignment\": \"BOTTOM\",\n      \"wrapStrategy\": \"OVERFLOW_CELL\",\n      \"textFormat\": {\n        \"foregroundColor\": {},\n        \"fontFamily\": \"arial,sans,sans-serif\",\n        \"fontSize\": 10,\n        \"bold\": false,\n        \"italic\": false,\n        \"strikethrough\": false,\n        \"underline\": false,\n        \"foregroundColorStyle\": {\n          \"rgbColor\": {}\n        }\n      },\n      \"backgroundColorStyle\": {\n        \"rgbColor\": {\n          \"red\": 1,\n          \"green\": 1,\n          \"blue\": 1\n        }\n      }\n    },\n    \"spreadsheetTheme\": {\n      \"primaryFontFamily\": \"Arial\",\n      \"themeColors\": [\n        {\n          \"colorType\": \"TEXT\",\n          \"color\": {\n            \"rgbColor\": {}\n          }\n        },\n        {\n          \"colorType\": \"BACKGROUND\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 1,\n              \"green\": 1,\n              \"blue\": 1\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT1\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.25882354,\n              \"green\": 0.52156866,\n              \"blue\": 0.95686275\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT2\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.91764706,\n              \"green\": 0.2627451,\n              \"blue\": 0.20784314\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT3\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.9843137,\n              \"green\": 0.7372549,\n              \"blue\": 0.015686275\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT4\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.20392157,\n              \"green\": 0.65882355,\n              \"blue\": 0.3254902\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT5\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 1,\n              \"green\": 0.42745098,\n              \"blue\": 0.003921569\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT6\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.27450982,\n              \"green\": 0.7411765,\n              \"blue\": 0.7764706\n            }\n          }\n        },\n        {\n          \"colorType\": \"LINK\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.06666667,\n              \"green\": 0.33333334,\n              \"blue\": 0.8\n            }\n          }\n        }\n      ]\n    }\n  },\n  \"sheets\": [\n    {\n      \"properties\": {\n        \"sheetId\": 0,\n        \"title\": \"Sheet1\",\n        \"index\": 0,\n        \"sheetType\": \"GRID\",\n        \"gridProperties\": {\n          \"rowCount\": 1000,\n          \"columnCount\": 26\n        }\n      }\n    }\n  ],\n  \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1ayf3YSjjjwDhFkUr3nrdN1JRScl3cOMB9lDbHEkjoko/edit\"\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "GET",
+                "uri": "https://sheets.googleapis.com/v4/spreadsheets/1ayf3YSjjjwDhFkUr3nrdN1JRScl3cOMB9lDbHEkjoko?includeGridData=false",
+                "body": null,
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "Date": [
+                        "Wed, 21 Feb 2024 21:21:09 GMT"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "x-l2-request-path": [
+                        "l2-managed-6"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "Vary": [
+                        "Origin",
+                        "X-Origin",
+                        "Referer"
+                    ],
+                    "Cache-Control": [
+                        "private"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "content-length": [
+                        "3330"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"spreadsheetId\": \"1ayf3YSjjjwDhFkUr3nrdN1JRScl3cOMB9lDbHEkjoko\",\n  \"properties\": {\n    \"title\": \"Test ClientTest test_add_timeout\",\n    \"locale\": \"en_US\",\n    \"autoRecalc\": \"ON_CHANGE\",\n    \"timeZone\": \"Etc/GMT\",\n    \"defaultFormat\": {\n      \"backgroundColor\": {\n        \"red\": 1,\n        \"green\": 1,\n        \"blue\": 1\n      },\n      \"padding\": {\n        \"top\": 2,\n        \"right\": 3,\n        \"bottom\": 2,\n        \"left\": 3\n      },\n      \"verticalAlignment\": \"BOTTOM\",\n      \"wrapStrategy\": \"OVERFLOW_CELL\",\n      \"textFormat\": {\n        \"foregroundColor\": {},\n        \"fontFamily\": \"arial,sans,sans-serif\",\n        \"fontSize\": 10,\n        \"bold\": false,\n        \"italic\": false,\n        \"strikethrough\": false,\n        \"underline\": false,\n        \"foregroundColorStyle\": {\n          \"rgbColor\": {}\n        }\n      },\n      \"backgroundColorStyle\": {\n        \"rgbColor\": {\n          \"red\": 1,\n          \"green\": 1,\n          \"blue\": 1\n        }\n      }\n    },\n    \"spreadsheetTheme\": {\n      \"primaryFontFamily\": \"Arial\",\n      \"themeColors\": [\n        {\n          \"colorType\": \"TEXT\",\n          \"color\": {\n            \"rgbColor\": {}\n          }\n        },\n        {\n          \"colorType\": \"BACKGROUND\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 1,\n              \"green\": 1,\n              \"blue\": 1\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT1\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.25882354,\n              \"green\": 0.52156866,\n              \"blue\": 0.95686275\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT2\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.91764706,\n              \"green\": 0.2627451,\n              \"blue\": 0.20784314\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT3\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.9843137,\n              \"green\": 0.7372549,\n              \"blue\": 0.015686275\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT4\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.20392157,\n              \"green\": 0.65882355,\n              \"blue\": 0.3254902\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT5\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 1,\n              \"green\": 0.42745098,\n              \"blue\": 0.003921569\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT6\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.27450982,\n              \"green\": 0.7411765,\n              \"blue\": 0.7764706\n            }\n          }\n        },\n        {\n          \"colorType\": \"LINK\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.06666667,\n              \"green\": 0.33333334,\n              \"blue\": 0.8\n            }\n          }\n        }\n      ]\n    }\n  },\n  \"sheets\": [\n    {\n      \"properties\": {\n        \"sheetId\": 0,\n        \"title\": \"Sheet1\",\n        \"index\": 0,\n        \"sheetType\": \"GRID\",\n        \"gridProperties\": {\n          \"rowCount\": 1000,\n          \"columnCount\": 26\n        }\n      }\n    }\n  ],\n  \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1ayf3YSjjjwDhFkUr3nrdN1JRScl3cOMB9lDbHEkjoko/edit\"\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "DELETE",
+                "uri": "https://www.googleapis.com/drive/v3/files/1ayf3YSjjjwDhFkUr3nrdN1JRScl3cOMB9lDbHEkjoko?supportsAllDrives=True",
+                "body": null,
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "Content-Length": [
+                        "0"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 204,
+                    "message": "No Content"
+                },
+                "headers": {
+                    "Date": [
+                        "Wed, 21 Feb 2024 21:21:09 GMT"
+                    ],
+                    "Content-Length": [
+                        "0"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "Vary": [
+                        "Origin, X-Origin"
+                    ],
+                    "Pragma": [
+                        "no-cache"
+                    ],
+                    "Expires": [
+                        "Mon, 01 Jan 1990 00:00:00 GMT"
+                    ],
+                    "Cache-Control": [
+                        "no-cache, no-store, max-age=0, must-revalidate"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "text/html"
+                    ]
+                },
+                "body": {
+                    "string": ""
+                }
+            }
+        }
+    ]
+}
diff --git a/tests/client_test.py b/tests/client_test.py
index 32ef9db..bf90926 100644
--- a/tests/client_test.py
+++ b/tests/client_test.py
@@ -1,3 +1,5 @@
+import time
+
 import pytest
 
 import gspread
@@ -145,3 +147,19 @@ class ClientTest(GspreadTest):
         self.assertEqual(
             values[0], res_values, "exported values are not the value initially set"
         )
+
+    @pytest.mark.vcr()
+    def test_add_timeout(self):
+        """Test the method to set the HTTP request timeout"""
+
+        # So far it took 0.17 seconds to fetch the metadata with my connection.
+        # Once recorded it takes 0.001 seconds to run it, so 1 second should be a large enough value
+        timeout = 1
+        self.gc.set_timeout(timeout)
+        start = time.time()
+        self.spreadsheet.fetch_sheet_metadata()
+        end = time.time()
+
+        self.assertLessEqual(
+            end - start, timeout, "Request took longer than the set timeout value"
+        )