Repository: ethereum/web3.py

Issue:
Make `HTTPProvider.get_request_headers()` a class-level method
### What feature should we add?

At the time of writing this issue, the method is defined here:
https://github.com/ethereum/web3.py/blob/main/web3/providers/rpc/rpc.py#L119-L123

It is:

```python
    def get_request_headers(self) -> Dict[str, str]:
        return {
            "Content-Type": "application/json",
            "User-Agent": construct_user_agent(str(type(self))),
        }
```

I am requesting this method be exposed at the class-level, to be re-written as:

```python
    @classmethod
    def get_request_headers(cls) -> Dict[str, str]:
        return {
            "Content-Type": "application/json",
            "User-Agent": construct_user_agent(str(cls)),
        }
```

Use-case: This allows me to grab the default headers _before_ constructing an `HTTPProvider` object for the purpose of modifying them / appending to them, without having to redefine.

Discussion:

Patch:
diff --git a/newsfragments/3467.feature.rst b/newsfragments/3467.feature.rst
new file mode 100644
index 00000000..b309f20b
--- /dev/null
+++ b/newsfragments/3467.feature.rst
@@ -0,0 +1,1 @@
+HTTPProvider and AsyncHTTPProvider's get_request_headers is now available on both the class and the instance
diff --git a/web3/_utils/http.py b/web3/_utils/http.py
index a5d79e1d..e3e266fc 100644
--- a/web3/_utils/http.py
+++ b/web3/_utils/http.py
@@ -1,9 +1,12 @@
 DEFAULT_HTTP_TIMEOUT = 30.0
 
 
-def construct_user_agent(class_type: type) -> str:
+def construct_user_agent(
+    module: str,
+    class_name: str,
+) -> str:
     from web3 import (
         __version__ as web3_version,
     )
 
-    return f"web3.py/{web3_version}/{class_type.__module__}.{class_type.__qualname__}"
+    return f"web3.py/{web3_version}/{module}.{class_name}"
diff --git a/web3/providers/rpc/async_rpc.py b/web3/providers/rpc/async_rpc.py
index bd485b45..8f723138 100644
--- a/web3/providers/rpc/async_rpc.py
+++ b/web3/providers/rpc/async_rpc.py
@@ -19,6 +19,7 @@ from eth_typing import (
     URI,
 )
 from eth_utils import (
+    combomethod,
     to_dict,
 )
 
@@ -108,10 +109,17 @@ class AsyncHTTPProvider(AsyncJSONBaseProvider):
             yield "headers", self.get_request_headers()
         yield from self._request_kwargs.items()
 
-    def get_request_headers(self) -> Dict[str, str]:
+    @combomethod
+    def get_request_headers(cls) -> Dict[str, str]:
+        if isinstance(cls, AsyncHTTPProvider):
+            cls_name = cls.__class__.__name__
+        else:
+            cls_name = cls.__name__
+        module = cls.__module__
+
         return {
             "Content-Type": "application/json",
-            "User-Agent": construct_user_agent(type(self)),
+            "User-Agent": construct_user_agent(module, cls_name),
         }
 
     async def _make_request(self, method: RPCEndpoint, request_data: bytes) -> bytes:
diff --git a/web3/providers/rpc/rpc.py b/web3/providers/rpc/rpc.py
index 654d80c0..23593e84 100644
--- a/web3/providers/rpc/rpc.py
+++ b/web3/providers/rpc/rpc.py
@@ -16,6 +16,7 @@ from eth_typing import (
     URI,
 )
 from eth_utils import (
+    combomethod,
     to_dict,
 )
 import requests
@@ -116,10 +117,18 @@ class HTTPProvider(JSONBaseProvider):
             yield "headers", self.get_request_headers()
         yield from self._request_kwargs.items()
 
-    def get_request_headers(self) -> Dict[str, str]:
+    @combomethod
+    def get_request_headers(cls) -> Dict[str, str]:
+        if isinstance(cls, HTTPProvider):
+            cls_name = cls.__class__.__name__
+        else:
+            cls_name = cls.__name__
+
+        module = cls.__module__
+
         return {
             "Content-Type": "application/json",
-            "User-Agent": construct_user_agent(type(self)),
+            "User-Agent": construct_user_agent(module, cls_name),
         }
 
     def _make_request(self, method: RPCEndpoint, request_data: bytes) -> bytes:


Test patch:
diff --git a/tests/core/providers/test_async_http_provider.py b/tests/core/providers/test_async_http_provider.py
index 876e65b3..cd4b2d84 100644
--- a/tests/core/providers/test_async_http_provider.py
+++ b/tests/core/providers/test_async_http_provider.py
@@ -94,8 +94,8 @@ async def test_async_user_provided_session() -> None:
     assert cached_session == session
 
 
-def test_get_request_headers():
-    provider = AsyncHTTPProvider()
+@pytest.mark.parametrize("provider", (AsyncHTTPProvider(), AsyncHTTPProvider))
+def test_get_request_headers(provider):
     headers = provider.get_request_headers()
     assert len(headers) == 2
     assert headers["Content-Type"] == "application/json"
diff --git a/tests/core/providers/test_http_provider.py b/tests/core/providers/test_http_provider.py
index 3a88bef6..82a982de 100644
--- a/tests/core/providers/test_http_provider.py
+++ b/tests/core/providers/test_http_provider.py
@@ -101,8 +101,8 @@ def test_user_provided_session():
     assert adapter._pool_maxsize == 20
 
 
-def test_get_request_headers():
-    provider = HTTPProvider()
+@pytest.mark.parametrize("provider", (HTTPProvider(), HTTPProvider))
+def test_get_request_headers(provider):
     headers = provider.get_request_headers()
     assert len(headers) == 2
     assert headers["Content-Type"] == "application/json"